Lean Programming Interfaces for Classic Graphics
This version of the document is dated 2026-02-17.
Notes on developing an application programming interface (API) for classic graphics with as few entry points as possible. As given in my specification for such graphics, classic graphics generally means 2-D or 3-D graphics achieved by video games before the year 2000 and the rise of programmable “shaders”. In general, pre-2000 graphics involve a game screen of 640 × 480 or smaller, up to 12,800 3-D polygons at a time (fewer if the game screen is smaller), and tile- or sprite-based 2-D graphics.
Goal: An Open-Source Engine or API for Classic Graphics
It would be of interest to write a free and open-source graphics engine that implements this specification and renders graphics in software (with a minimum of source code and dependencies),1 or to establish a lean API for this specification. The following are examples:
- Quake (1996), Quake II (1997), and Quake III Arena (1999) popularized the practice of using only a subset of the OpenGL 1.1 API for a game’s graphics rendering (see, for example, “Optimizing OpenGL drivers for Quake3” by the developer of Quake III Arena).
- The API reference for the two-dimensional (2-D) game engine Pyxel. But, in addition to the efforts there, a minimal version of the Python language runtime and nonreliance on hardware acceleration (notably the OpenGL API) would be worthwhile.
- BGI, a 2-D graphics API (
graphics.h) by what was then known as Borland.
The graphics engine is intended to run even on computers from around 2005 (and maybe even on older computers), and with low resources, and so to enable video games that run with acceptable performance on those computers.
2-D Graphics
The simplest way to proceed is to give the application a frame buffer, a block of memory consisting of a rectangular array of pixel samples. The nature of each pixel sample depends on the game’s needs; for example, it may be an 8-bit index to a color palette, or a 16-bit color value. In this case, the application must render graphics in software.1
Note: Under the classic-graphics specification, the frame buffer has no more than 307,200 pixels.
A tile- and sprite-based API suggested by the classic-graphics specification is yet to be determined.
The following is a sketch of what could be included in a lean API for copying and stretching 2-D images as well as for geometric drawing.2 (For such an API, antialiasing support is optional, and it’s enough for the API to draw to images stored in system memory, and not also drawing to video memory or directly to the screen.)
- Getting and setting pixel values of an image.
- Filling with a solid color a rectangular area (whose edges have integer coordinates) of an image.
- Copying a rectangular area (whose edges have integer coordinates) of an image onto another image, with optional nearest-neighbor scaling. The copying can optionally exclude transparent pixels or pixels of a certain color.
- Filling 2-D paths with a solid color, with even/odd or nonzero winding order. 2-D paths are sequences of path segments (line segments, quadratic Bézier curves, cubic Bézier curves, and elliptical arcs).
- Drawing one-unit-thick outlines of 2-D paths with a solid color.3
- Flood filling colored areas of an image.
- Optionally, “inverting” the colors or color indices of an image’s rectangular area (whose edges have integer coordinates).
A leaner API could provide for the following instead:
- Getting and setting pixel values of an image.
- Filling the following figures with a solid color.
- Rectangles whose edges have integer coordinates, and ellipses that are tightly contained in them.
- Polygons with integer coordinates and even/odd or nonzero winding order. The API can choose to support arbitrary polygons, convex polygons only, or monotone-vertical polygons only.4
- Drawing one-unit-thick line segments with a solid color, supporting only integer coordinates.
- Flood filling colored areas of an image.
- Optionally:
- “Inverting” the colors or color indices of an image’s rectangular area (whose edges have integer coordinates).
- Drawing one-unit-thick elliptical arcs with a solid color, supporting only integer coordinates.5
The following is not included in either API.
- Color palette functions. Images may or may not have a color table, and the application is assumed to handle colors on an image-by-image basis. (There is the matter of sending finished images to the user’s display, which may vary in the number of colors it supports, but the lean 2-D API need not be concerned about this.)
- Text rendering, since the needs of applications in supporting writing systems and languages vary, as do approaches to rendering text.6
3-D Graphics
Unlike with today’s programmable “shaders”, classic 3-D video-game graphics support only simple, yet admirable capabilities for real-time rendering of three-dimensional scenes, as well as a low scene complexity (fewer than 20,000 triangles per frame).
This section gives suggestions for a lean API supporting 3-D graphics. Any implementation of it should render graphics in software1 and optionally with hardware acceleration. As with 2-D, the 3-D API is allowed to support drawing to images stored in system memory without also supporting drawing to video memory or directly to the screen.
The classic-graphics specification recognizes the following 3-D graphics capabilities as within its spirit:
- Z buffering (depth buffering).
- Bilinear filtering.
- Flat shading and Gouraud shading.
- Perspective correction.
- Per-vertex specular highlighting.
- Per-vertex depth-based fog.
- Line drawing.
- Two-texture blending.
- Edge antialiasing (smoothing).
- MIP mapping.
- Source and destination alpha blending.
The PC 99 System Design Guide sections 14.27 to 14.34 (except for the screen resolution, frame rate, and double buffering requirements) are also in scope.
Stencil buffers, bump mapping, environment mapping, and three- or four-texture blending are borderline capabilities.
Suggested C-Language Functions
DrawTrianglesOneTex(State3D *state, float* vertices, uint32_t numvertices, uint32_t * indices, uint32_t numindices, Texture *texture);
Draws a sequence of triangles. The vertices array is a rectangular array of numbers organized into “vertex blocks”. The number of floats pointed to must equal the number of floats per vertex block times numvertices. The number of indices (numindices) must be a multiple of 3. float is a number in IEEE 754 binary32 format.
Note: As given in the classic graphics specification, the number of vertices per frame should be no more than 38,400 for a screen resolution of 640 × 480.
There are several possibilities for “vertex blocks”:
- Each “vertex block” has eight
floatvalues: the x-, y-, and z-coordinates; the normal vector’s X, Y, and Z components; and the texture coordinates (U and V). - Each “vertex block” has five
floatvalues: the x-, y-, and z-coordinates; and the two texture coordinates. This is for games that render only textured triangles and calculate their own lighting.
state is the 3-D graphics state, to be determined. This state will include the game’s frame buffer, and possibly additional parameters yet to be determined.
texture is information about the texture, to be determined. This information will include the texture’s image data and a blending operation (add, modulate, etc.).
DrawTriangleFanOneTex(State3D *state, float* vertices, uint32_t numvertices, Texture *texture);
Draws a triangle fan. vertices, texture, and numvertices are as in DrawTrianglesOneTex. Moreover, numvertices must be 3 or greater.
DrawTriangleStripOneTex(State3D *state, float* vertices, uint32_t numvertices, Texture *texture);
Draws a triangle strip. vertices, texture, and numvertices are as in DrawTrianglesOneTex. Moreover, numvertices must be 3 or greater.
DrawTrianglesTwoTex(State3D *state, float* vertices, uint32_t numvertices, uint32_t * indices, uint32_t numindices, Texture *texture1, Texture *texture2); DrawTriangleFanTwoTex(State3D *state, float* vertices, uint32_t numvertices, Texture *texture1, Texture *texture2); DrawTriangleStripTwoTex(State3D *state, float* vertices, uint32_t numvertices, Texture *texture1, Texture *texture2);
Like the corresponding ...OneTex versions, with the following exceptions. Each vertex block has ten float values: the x-, y-, and z-coordinates; the normal vector’s X, Y, and Z components; the texture coordinates (U and V) for texture1; and the texture coordinates for texture2. These functions are suggested here because some games from the late 1990s rely on so-called light-map textures and two-texture blending rather than in-game lighting calculations.
DrawTriangles(State3D *state, float* vertices, uint32_t numvertices, uint32_t * indices, uint32_t numindices); DrawTriangleFan(State3D *state, float* vertices, uint32_t numvertices); DrawTriangleStrip(State3D *state, float* vertices, uint32_t numvertices);
Draws triangles without textures. Like the corresponding ...OneTex versions, with the following exceptions. Each “vertex block” has six float values: the x-, y-, and z-coordinates; and a color’s red, green, and blue values, each ranging from 0 through 1. This is for games that render only triangles without textures. These functions are suggested here because some games from the mid- to late 1990s often draw polygons without textures.
This is far from a complete list of useful 3-D drawing functions; there may be others, but the goal is to define a compact set of functions supporting only those 3-D capabilities actually used by video games in the 1990s and earlier.
Further Reading
- Microsoft C/C++ Version 7.0 Run-Time Library Reference (1991), section 2.6 (“Microsoft C runtime”). Describes the graphics routines the named library supports.
- The Windows graphics device interface (GDI):
- The GDI supported in Windows 95, Windows 98, and Windows 3.x (the “16-bit GDI”), along with its limitations, is described in the following references:
- “Microsoft Windows Software Development Kit: Programmer’s Reference” (for Windows 3.1).
- Charles Petzold, “Programming Windows 3.1: The Microsoft Guide to Writing Applications for Windows 3.1”, Microsoft Press, 1992.
- Microsoft Win32 Programmer’s Reference (for Windows 95 and Windows 98).
- “Microsoft Win32 API Overview”, Microsoft Developer Network, February 1994.
- “INFO: Developing Win32-Based GDI Apps for Windows 95 and Windows NT or Windows2000”, Microsoft Knowledge Base article Q136989.
- The Windows NT GDI is described in the following references. Its dashed-line feature did not materially change between the two references.
- Chapter 3 of the Graphics Driver Design Guide, part of the Windows NT 4.0 Device Driver Kit.
- The archived documentation for the XDDM, the driver model supported in Windows 2000, Windows XP, Windows Vista, and Windows 7.
- Windows CE 2.0’s version of the GDI and its 2-D graphics features are described in Jon Christiansen, “Microsoft Windows CE Graphics Features” (undated, about 1997). Windows CE was an operating system for embedded and handheld computers.
- The GDI supported in Windows 95, Windows 98, and Windows 3.x (the “16-bit GDI”), along with its limitations, is described in the following references:
License
Any copyright to this page is released to the Public Domain. In case this is not possible, this page is also licensed under Creative Commons Zero.
Notes
-
In this document, “rendering in software” means that the rendering of graphics does not rely on a video card, a graphics accelerator chip, or the operating system’s graphics API (such as GDI, OpenGL, or Direct3D) with the sole exception of sending a finished game screen image to the player’s display (such as through GDI’s
StretchDIBitsor copying to VGA’s video memory).
The following are examples of a graphics library that follows the spirit, even if not the letter, of the classic-graphics specification: Tilengine, kit, DOS-like, raylib’srlswsoftware renderer. Michal Strehovský published an interesting technique to create small game applications, and so did Jani Peltonen. ↩ ↩2 ↩3 -
It is unclear whether to include any of the following in the lean 2-D graphics API; this will depend on how heavily pre-2000 or pre-1995 games made use of them.
a. 2-D affine transformations (which keep parallel lines parallel; examples are scalings, shears, and rotations).
b. Translations, rotations and scalings, but no other 2-D affine transformations.
c. Translations, rotations, scalings, and reflections, but no other 2-D affine transformations.
d. So-called “raster operations” (bit-by-bit operations between two images), such as those found in the Windows API (for example,BitBit) and in the “Microsoft C runtime” (for example,_putimage,_setwritemode).
e. Alpha compositing while drawing 2-D graphics.
f. Drawing one image part over another (Porter and Duff’s “over” operation), with optional translucency, but no other nontrivial alpha compositing. The images may have translucent (semitransparent) or transparent pixels.
g. Same as (f), except the images can’t have translucent pixels.
h. Drawing dashed 2-D paths.
i. Filling rectangular areas with a repeating image pattern without transparent or translucent pixels.
j. Filling rectangular areas with a repeating image pattern. The image may have transparent pixels.
k. Filling arbitrary 2-D paths with a repeating image pattern. The image may have transparent pixels.
Note on points “a” to “c”: Unlike the Windows NT GDI, the 16-bit GDI did not support arbitrary affine transformations; notably, it did not support rotations or shears (for references, see “Further Reading”).
Note on point “h”: Dashed 2-D paths are also known as “styled lines”. One-unit-thick outlines are dashed (“styled”) by leaving out pixels (examples include the_getlinestylemethod of the “Microsoft C runtime”, or the sophisticated approach in the Windows NT GDI [see “Further Reading”] that depends on the horizontal and vertical spacing between pixels). More general outlines of a 2-D path are dashed by leaving out parts of the path (an example is found in Windows NT’s model).
Note on points “i” to “k”: Windows’sCreatePatternBrushfunction for creating repeating image patterns supported only 8 × 8 or smaller images in Windows 95 and Windows 3.x. ↩ -
Here, a “unit” means the spacing between an image’s pixels. Thicker outlines can be drawn by approximating the 2-D path with line segments, then drawing filled circles around each segment’s endpoints, then drawing filled rectangles that follow the path of each line segment. Thus, a lean graphics API need not support outlining paths thicker than one unit. See also Ron Gery, “Primitive Cool”, Microsoft Developer Network, Mar. 17, 1992. ↩
-
A “monotone-vertical” polygon is one that changes direction along the y-axis exactly twice, whether or not the polygon is self-intersecting. Every convex polygon is monotone-vertical. See chapter 41 of Michael Abrash’s Graphics Programming Black Book Special Edition, 1997. ↩
-
Elliptical arcs were not supported in Windows CE version 2.0 and earlier, unlike Windows 95, Windows NT, and Windows 3.x. See Jon Christiansen, “Microsoft Windows CE Graphics Features”. ↩
-
Text rendering is essentially the drawing of glyphs of text. A glyph is a writing element of a font; examples are letters, digits, the i’s dot, and the f-f-l combination. Before 2000, there were three kinds of fonts for screen display: raster fonts (the glyphs are images); vector fonts (the glyphs are made of line segments and/or curves; Hershey, Modern, and Script are examples); and outline fonts (the glyphs are filled 2-D paths; TrueType fonts are examples). (Later developments saw (1) the addition of scalable colored graphics, especially emoji, to outline fonts and (2) “subpixel” antialiasing of glyphs, such as the ClearType technology announced in November 1998.) The conversion of text to glyphs and the positioning of such glyphs is often nontrivial. ↩