CGSX v0.1.2                                        John Elliott, 27 March 2025
==============================================================================

  CGSX is an attempt to implement CP/M GSX graphic drawing capabilities in 
a reasonably self-contained C library. The intended use case is for CP/M 
emulators, but of course that doesn't rule out other uses I haven't thought
of.

  The library is licensed under the MIT licence.

What's new
==========
[0.1.2] Added two new TEK4014 drivers, aimed at xterm's TEK4014 emulation.

[0.1.1] The PDF backend has been modified to support older versions of libHaru,
and some variable declarations have been reordered so the library can be 
compiled by a C89 compiler.

In use
======

  There are two ways in which the library can be used:

i) As a self-contained GSX implementation (GDOS and GIOS)
ii) As a collection of GSX drivers (GIOS).

GDOS
====
  When using CGSX as a complete GSX implementation, the steps to take are:

1. Create the GDOS using gdos_create(). This will return 0 if succeeded, -1
  if it failed due to lack of memory.

2. Register one or more devices with the GDOS. This is done using 
  gdos_register() or gdos_register_raster(). If you register multiple 
  devices, each one should have its own device number. By convention, device
  1 is the screen, 11 is the plotter and 21 is the printer. 

   gdos_register() is passed a function that creates an instance of the 
  required driver when requested. gdos_register_raster() is passed a 
  pointer to a RASTERSURFACE on which output will be rendered.

3. When you handle a GSX call, extract its arguments into memory arrays 
  and pass them to gdos_dispatch(). In practice it is safest to 
  start by populating the contrl array, then call check_contrl() on it
  before using the array counts in it to populate intin and ptsin. Some 
  programs that use GSX don't bother to populate the intin array count 
  for calls where they think it doesn't matter.

4. Once gdos_dispatch() has returned, copy the contrl, intout and ptsout
  arrays back into the emulator's memory. It is best to copy contrl first,
  in case it overlaps with the other two.

5. Free the GDOS (if required) with gdos_destroy().

  Note that if you are writing a CP/M-80 emulator that traps BDOS call 73h and
passes it to gdos_dispatch(), you should remove the GSX-80 GDOS from programs.
This can be done either manually by removing the first 0x380 bytes of the .COM
file, or (if you have control of the program loader) by searching for a 
suitable code signature and automatically skipping the first 0x380 bytes of
the .COM file if it is found.

GIOS
====
  In this scenario, the original GSX-80 GDOS remains, using custom drivers
that use an emulator trap to pass control to CGSX. DE will hold the address
of the GSX parameter block.

1. If you are using the raster driver, create a RASTERSURFACE structure 
  describing the surface it's going to be drawing on. For more detail on 
  this, see below. If you are using the PDF driver, you will need a 
  PDFPARAM structure describing the page size and output directory. Both of
  these structures need to be in existence for at least as long as the GIOS 
  driver is, so having them as local stack variables is unlikely to work.

2. When your emulator trap is triggered, read the input arrays into memory.
 As when using gdos_dispatch(), it's safest to read the contrl array, use 
 check_contrl() to sanitise it, and then load the intin and ptsin arrays.

3. The first call should be number 1, Open Workstation. At this point 
  (or before) create your driver using the appropriate gios_*_create()
  function. This will populate a pointer to a GIOSDEVICE. Pass NULL as the
  first parameter to indicate that there is no GDOS.

4. Call the 'dispatch' function pointer on the GIOS to process the request.

5. When you receive call 2, Close Workstation, call the 'dispatch' function
  pointer in the usual way, and then the 'destroy' function pointer.

Examples
========
  test/gsxtest.c is an example of an implementation using the library's GDOS.
It's a minimal Z80 emulator providing just enough of the CP/M API to run 
my CP/M GSX tester, GSXTEST.COM. The BDOS handler (lines 170-228) extracts
the parameters of the GSX call from Z80 memory, passes them to gdos_dispatch(),
and copies the results back into Z80 memory.

  sample.zsm is an example of a GSX driver (in this case, targeted at my
emulator JOYCE) using an emulator trap to hand off to the library's GIOS.
  
Driver Implementations
======================

  Drivers provided by CGSX drivers fall into two classes: Raster drivers 
and vector drivers.

  A raster driver covers most situations where the output device can be 
modelled as a grid of pixels. CGSX takes drawing operations and converts
them into simple set/get pixel commands, which are then passed to a 
RASTERSURFACE object. This is the easiest approach for producing output on 
pixel-based displays, dot-matrix printers, bitmap files and so on. Since all
raster drivers share the same rendering code, they tend to be quite similar
in what features they support; they all support text rotation, filled 
areas, reading and writing cell arrays, wide lines, five line styles, 
six halftone fills, six pattern fills, six marker styles, four General Drawing
Primitives, and so on.

  If you are using a GDOS, raster drivers are registered with 
gdos_register_raster(). If you are using the drivers standalone, then 
they are created with gios_raster_create(), passing the RASTERSURFACE 
as the third parameter. The RASTERSURFACE will need to exist for the 
lifespan of the GDOS or driver.

  A vector driver is used for devices where the standard conversion of GSX 
operations to pixel plotting isn't appropriate. For example, if output is 
being written to a vector-based file such as PDF.

  Vector drivers are registered with gdos_register(), or created using their
specific gios_*_create() function. The meaning of the third parameter to 
the create function will depend on the driver.

  Drivers provided by CGSX are:

Vector driver: PDF
------------------
  The PDF driver uses libHaru <https://libharu.org/> to generate its 
output, so you will need libHaru installed to use it. It is an output-only
driver, writing each page of output to a separate PDF page. Nearly all GSX 
features can be rendered, with the exception of the XOR writing mode, which 
the PDF format does not seem to support.

  It is created with gios_pdf_create(). The third parameter should be a 
static PDFPARAM object giving the required page size in points and the
directory to which PDF output should be sent.

Vector driver: Tektronix 4014
-----------------------------
  Unusually, CGSX provides both a vector and a raster driver for the 
Tektronix 4014 emulation provided by xterm. The vector driver is faster 
(since it directly accesses native features of the terminal) but more 
limited (since it can only support those features provided natively by 
the terminal). Basically, its support is limited to lines, text and markers:

* No filled areas
* No cell arrays
* No General Drawing Primitives
* Only one line width
* No text rotation
* No ability to change the drawing colour. Lines, markers and text are 
 always drawn in white on a black background.
* No ability to change the drawing mode. The Tektronix terminal does not
 allow pixels to be erased once they are drawn, except by clearing the 
 whole screen.

  The text mode escapes are supported, using the host xterm as the text 
screen. There is also support for locator input in request mode. Use the 
mouse to point at the appropriate spot, and use the spacebar as the mouse
button.

  The Tektronix drivers default to writing control codes to standard output,
and reading the results from standard input. This is usually the desired 
behaviour; if you want to use an alternative channel, pass a TEK4014PARAM
containing function pointers for the alternative output and input methods.
For the default behaviour, the parameter pointer can be left as NULL. Note
that the input channel must return characters as soon as they are received,
rather than buffering them (if a terminal, it should be in RAW or CBREAK 
mode).
 
Raster driver: PBM
------------------

  The PBM driver is probably the simplest possible raster output device 
currently implemented. It generates output in the form of a series of .PBM
(Portable BitMap) files. The caller can specify whether it defaults to a 
white on black colour scheme (as GSX expects of displays) or black on white 
(as it expects of printers). PBM files are not compressed, so this will 
result in somewhat large files.

  To use it, create a PBM surface with rdev_pbm_create(), and either 
register it with the GDOS using gdos_register_raster(), or create a 
raster device directly with gios_raster_create().

Raster driver: PPM
------------------

  PPM is the colour equivalent of the PBM driver, outputting a 24-bit 
colour bitmap rather than a black and white bitmap. PPM files also have
no compression, so the files are even larger than their PBM counterparts.

  To use it, create a PPM surface with rdev_ppm_create(), and then 
register it with gdos_register_raster() or use it with gios_raster_create() 
as appropriate.

Raster driver: PNG
------------------
  If libpng and its headers are available, it is possible to generate 24-bit
colour PNG files. This driver is almost identical to the PPM driver, except
for the output file format.

  To use it, create a PNG surface with rdev_png_create(), and then 
register it with gdos_register_raster() or use it with gios_raster_create() 
as appropriate.

Raster driver: Tektronix 4014
-----------------------------
  This is the second driver for the xterm Tektronix 4014 emulation. Rather 
than having GSX operations draw directly on the screen, they are rendered to
a memory buffer, and the actual display is updated pixel by pixel. If a pixel 
changes colour from black to white, this is shown on the screen instantly. 
However changes from white to black are deferred until the next Update 
Workstation call is issued, at which point if there are any such changes 
pending the entire screen must be withdrawn. 

  On a real Tektronix display this would probably be punishingly slow, but 
on a modern xterm performance is resonably good.

  Like the vector Tektronix driver, this supports text mode (displayed on 
the originating xterm window) and locator input in request mode.

  When creating the surface with rdev_tek4014_create(), the outputfunc and
inputfunc parameters would normally be left as NULL, in which case output
will be sent to stdout and input will be read from stdin. Otherwise, as 
the the vector Tektronix driver, you will need to provide your own versions
of these.

Creating your own driver: Vector
================================

  If you want to create your own vector driver, you should provide a 'create'
function matching this signature:

int gios_xxx_create(PGDOS self, GIOSDEVICE **ptr, void *param);

  This should allocate and initialise the data needed by your driver, which 
would begin with a GIOSDEVICE. The GIOSDEVICE would contain two function
pointers:

void (*destroy)(GIOSDEVICE **ptr)

	This should free all the memory and any other resources associated
	with the GIOSDEVICE at (*ptr), then set (*ptr) to NULL.

int (*dispatch)(GIOSDEVICE *self, gword *contrl, gword *intin, gword *ptsin, 
	gword *intout, gword *ptsout)

	Perform a GSX operation. Any pixel coordinates will be in the units 
	used by your device. contrl[0] will be the GSX function, 1-33.

  Because vector devices can differ so wildly, it's not possible to write a 
great deal more about how to implement one.

Creating your own driver: Raster
================================

  To create a raster driver, you create a RASTERSURFACE and pass it to 
gdos_register_raster() or gios_raster_create(). The RASTERSURFACE, since it
models the screen, is expected to have a longer lifespan than the GSX GIOS 
or GDOS; you will need to call its destroy() method once you are sure it is
no longer required.

  The RASTERSURFACE could be a wrapper around an output file format, a 
terminal, or an emulated bitmap display. It has more methods than GIOSDEVICE,
but they are lower-level operations that should be reasonably simple to
implement:

void (*destroy)(struct RasterSurface **pself);

	Call this to delete this surface and free any associated memory.
	Note that the lifespan of a RasterSurface is expected to be longer
	than the associated GSX driver, just as on a real CP/M computer the
	same display may persist through several calls to GSX.

int (*dispatch)(struct RasterSurface *self, GIOSDEVICE *caller,
			gword *contrl, gword *intin, gword *ptsin, 
			gword *intout, gword *ptsout);

	This allows the raster surface to handle any GSX call before the
	default implementation in the driver is used. It can be used, for
	example, to add support for pointing devices, or (if the device has
	a text mode) implementing escape functions using the text mode.

	It also allows the raster surface to block any functions that should
	not be implemented. The example output drivers use this to report 
	that there is no text screen by returning (-1,-1) to GSX function 5
	subfunction 1. A driver could also block all attempts to remap
	colours (for example, if it was emulating a monochrome printer) by
	implenting function 14 as a no-op.

	Return 0 if the function should be handled by the default 
	implementation, 1 if the function has been handled here.

	If you want to call the default implementation and then perform 
	additional processing, here's one way to do it:

		/* do initial processing */
		self->dispatch = NULL;
		(*caller->dispatch)(caller, contrl, intin, ptsin, intout, ptsout);
		self->dispatch = dispatchfunc;
		/* do final processing */
		return 1;

void (*dimensions)(struct RasterSurface *self, gword *width, gword *height)

	This returns the dimensions of the screen in pixels.
	
void (*pixelsize)(struct RasterSurface *self, gword *width, gword *height)

	This returns the size of a pixel in micrometres. It is used for 
	aspect ratio calculations when drawing shapes like circles.

void (*enter_g)(struct RasterSurface *self)

	If the device has separate text and graphics modes, select the 
	graphics mode. This function pointer can be left as NULL if the
	device doesn't have a separate text mode.

void (*exit_g)(struct RasterSurface *self)

	If the device has separate text and graphics modes, select the 
	graphics mode. This function pointer can be left as NULL if the
	device doesn't have a separate text mode.

void (*clear)(struct RasterSurface *self, unsigned ink)

	Blank the display / output buffer to the selected colour.
	
void (*flush)(struct RasterSurface *self, unsigned ink)

	On a device like a printer, this would finalize the current page and
	print it. The supplied raster drivers, in a similar way, write the 
	current image out as a bitmap file and clear the image buffer to 
	blank. If the display is to be cleared, 'ink' is the value to clear it
	to. An interactive device like a screen need not do anything here.

unsigned (*getpixel)(struct RasterSurface *self, gword x, gword y)

	Read a pixel from the screen. The value returned is the internal
	representation of the pixel, defined by the surface. The coordinate
	system used has the origin at the top left corner of the screen.

void (*setpixel)(struct RasterSurface *self, gword x, gword y, unsigned ink)

	Write a pixel to the screen. Note that the ink value may be outside
	the range returned by getpixel -- it may have been xor'ed with 
	another ink, or complemented to try and get an inverse.

unsigned (*has_palette)(struct RasterSurface *self)

	This should report whether the display has palette registers, or a 
	similar mechanism, that can change the colour of pixels already on 
	the screen. For example, on the ZX Spectrum colour 1 is always blue
	and this can't be changed, while on the BBC Micro colour 1 can be 
	displayed as blue, red, green etc. using the VDU 19 terminal code.

	Return 0 if there is no palette, otherwise the number of palette
	registers.

unsigned (*max_colours)(struct RasterSurface *self)

	This should return the maximum number of colours that can be 
	displayed simultaneously. For a monochrome device this would be 2.

void (*get_ink_rgb)(struct RasterSurface *self, unsigned ink, gword *r, 
	gword *g, gword *b)

	Convert an internal ink value to red/green/blue component values. The
	values use a scale of 0-1000 (inclusive).

void (*set_ink_rgb)(struct RasterSurface *self, unsigned ink, gword r, 
	gword g, gword b)

	If the device has palette registers, this is used to set the colour
	in which an ink is displayed. If the device has a fixed palette this
	function pointer can be left as null.

unsigned (*closest_rgb)(struct RasterSurface *self, gword r, gword g, gword b)

	Return the internal ink number that's the closest match for the 
	passed red/green/blue values. 

gword (*count_fonts)(struct RasterSurface *self)

	If the display has one or more native bitmap fonts, this should 
	return a count of them. If the display doesn't have any native 
	fonts, this function pointer can be left as null.

int (*select_font)(struct RasterSurface *self, GSXFONT *font)

	Populate the passed GSXFONT structure with the characteristics of
	the specified font. If the display doesn't have any native 
	fonts, this function pointer can be left as null.

Raster Devices and Text Mode
----------------------------  

  By default, the raster driver emulates the 'text mode' escape functions,
by simulating a text mode on the graphical screen. 

  A raster surface that has a true text mode can override these functions 
using its dispatch() entry point. The Tektronix raster driver does this, 
presenting the original xterm as the text mode with the Tektronix window 
as the graphics mode. 

  A raster surface that isn't representing an interactive screen (such as 
a printer) would also override the escape functions and return a screen 
size of -1 x -1 to indicate that there is no text mode support at all. This 
is what the provided PNG, PBM and PPM drivers do. 

