Skip to content

The card makers' 3D APIs: Glide, ATI3DCIF, S3D, SGL, and Direct3D 3 ​

In 1996 and 1997 each 3D chip had its own programming interface, and a game that wanted to run well on several cards shipped one renderer, often one whole executable, per card. This page covers the three most common of them in detail: 3dfx Glide 2.x, ATI's ATI3DCIF for the 3D Rage, and S3's S3D Toolkit for the ViRGE, with VideoLogic's SGL for the PowerVR in less detail. It says how to find each in a game, how to read the calls, how textures reach the card, and what the per-card builds tend to differ in. 3D accelerators has the overview of the cards and of Direct3D and OpenGL. One crate per chip holds the formats and constants named here (crates): gameframe-voodoo for 3dfx, gameframe-rage for ATI, gameframe-virge for S3, gameframe-powervr for PowerVR, with the texel decoder they share in gameframe-texel. Each chip crate lists the texel layouts its API takes (TEXTURE_FORMATS), and each also draws: gameframe-voodoo runs the whole Voodoo pipeline, while gameframe_rage::draw and gameframe_virge::draw draw the untextured, blended faces the ATI and S3 builds of the period gave their chips. Those two crates share gameframe-raster. gameframe-d3d runs Direct3D 3 execute buffers the same way ("Direct3D 3 execute buffers" below).

Find the API ​

List the imports (tools/pe/peinfo.py GAME.EXE):

LibraryAPIWhat the imports look like
glide.dll, glide2x.dllGlide 2.xMany decorated names: _grSstOpen@24, _grTexDownloadMipMap@16, _grDrawTriangle@12
glide3x.dllGlide 3 (1998 on)grSstWinOpen, grDrawVertexArray: a different API from Glide 2
ati3dcif.dllATI3DCIFA few functions: ATI3DCIF_Init, ContextCreate, ContextSetState, TextureReg, RenderBegin, RenderPrimList, RenderEnd
sgl.dllPowerVR SGLUndecorated __cdecl names: sgl_create_list, sgl_add_vertices, sgl_set_texture_effect, sgl_render
S3DTKW.dllS3D Toolkit (Windows)Four functions: S3DTK_InitLib, S3DTK_CreateRenderer, S3DTK_DestroyRenderer, S3DTK_LinearToPhysical; the rest come through a function table

Games linked these at load time, or loaded them with LoadLibrary and GetProcAddress so one executable could run without the card. Search the strings for the DLL names to find the second kind.

A few imports are a clue in themselves. A Glide build that does not import ddraw.dll draws everything through the Voodoo. An ATI3DCIF build that does not import ATI3DCIF_TextureReg never gives the chip a texture, so its textured surfaces are still drawn in software.

Read the state calls ​

All three APIs set the render state through one function and a number: ATI3DCIF_ContextSetState(context, id, &value), S3DTK_SetState(renderer, key, value), and Glide's many gr* calls with enumerated arguments. In the disassembly the number is a pushed constant just before the call. Name every call site:

  1. List the call sites. For a direct import, take the thunk's address from the export's functions.tsv and list the calls to it: tools/triage/insns.py GAME.EXE 'call 0xTHUNK'. For a function table (S3D), find the table's address and the calls through [table + 4 * slot].
  2. Read the constant pushed as the id or key at each site.
  3. Name it with the crate: ati::render_state_name(id), s3::state_name(key), glide::Format::from_u32(v). The S3D function table's slots are in s3::FUNCTIONS: slot 0 is S3DTK_SetState, slot 2 S3DTK_TriangleSet.
  4. Write the list into the topic document: which states the build sets, to which values.

The list says what the card was asked to do. A build that never sets the texture states (C3D_ERS_TMAP_EN, C3D_ERS_TMAP_SELECT, S3DTK_TEXTUREACTIVE) draws no textures on the chip. A build that only sets S3DTK_LITTEXTURE and S3DTK_UNLITTEXTURE, never the PERSPECT types, draws affine textures that swim on large polygons.

How textures reach the card ​

Glide 2.x ​

A Glide texture is a GrTexInfo: smallest and largest mip level (GR_LOD_256 = 0 down to GR_LOD_1 = 8), aspect ratio (GR_ASPECT_8x1 = 0 to GR_ASPECT_1x8 = 6, square = 3), format (GR_TEXFMT_*) and a data pointer. The game:

  1. asks for the space a texture needs, grTexCalcMemRequired(smallLod, largeLod, aspect, format), and places it at an address from grTexMinAddress(tmu) upward;
  2. uploads it with grTexDownloadMipMap(tmu, address, evenOdd, &info), and a palette or NCC table with grTexDownloadTable(tmu, type, data);
  3. selects it with grTexSource(tmu, address, evenOdd, &info) before drawing.

Games keep these four values per texture in a table of slots. Find it from the arguments of grTexSource and read it: each slot gives a texture's size and format directly (glide::size, glide::Format). A Voodoo Graphics card usually had 2 MB of texture memory on its one texture unit (TMU), so games fitted their textures to it. Look for:

  • Smaller levels for some textures. A flag per texture page in the data that sets largeLod to GR_LOD_128 instead of GR_LOD_256, with a loop that halves the page in place (every other texel of every other row) before the upload. resample::halve_point does the same.
  • 8-bit textures with a palette (GR_TEXFMT_P_8) for large pages such as fonts, and 16-bit RGB565 or ARGB1555/4444 for the rest. A palette entry is 0x00RRGGBB.
  • NCC textures (GR_TEXFMT_YIQ_422): 8 bits per texel through a table of 16 luminance values and 4 + 4 colour offsets (ncc::NccTable). Tools of the time compressed 24-bit art to it; the table travels with the texture.
  • The number of TMUs. grSstQueryHardware reports them. Some games hard-wire one and leave two-TMU paths unused. Check what the code writes before believing a branch is taken.

ATI3DCIF ​

A texture is a C3D_TMAP with its format (C3D_ETF_*: CI4, CI8, RGB1555, RGB565, RGB8888, RGB332, Y8, YUV422, RGB4444), size and pointers to its levels in DirectDraw surface memory. ATI3DCIF_TextureReg registers it and returns a handle; ContextSetState(C3D_ERS_TMAP_SELECT, &handle) selects it. The drawing surface is set with C3D_ERS_SURF_DRAW_PTR and C3D_ERS_SURF_DRAW_PITCH: often the same DirectDraw back buffer that the game's software renderer draws into. That lets a build mix the two, the chip for some faces and the CPU for others.

S3D Toolkit ​

S3DTK_CreateRenderer fills a S3DTK_FUNCTIONLIST with function pointers; all drawing goes through it. A texture is a S3DTK_SURFACE (sfOffset, sfWidth, sfHeight, sfFormat) in video memory. sfFormat holds S3DTK_TEXARGB8888, TEXARGB4444, TEXARGB1555 or TEXPALETTIZED8, with the S3DTK_TEXTURE flag. There is no RGB565 texture format. Games turn linear DirectDraw addresses into the chip's offsets with S3DTK_LinearToPhysical. Selecting a texture is SetState(S3DTK_TEXTUREACTIVE, &surface); the drawing kind is SetState(S3DTK_RENDERINGTYPE, …).

PowerVR SGL ​

VideoLogic's SGL (sgl.dll, version 3.1 in 1997) is a scene graph, not a triangle API. The game builds display lists of transforms, materials, meshes, lights and cameras once (sgl_create_list, sgl_create_mesh, sgl_add_vertices, sgl_set_texture_map), and each frame only edits a few of them (sgl_modify_transform, sgl_set_ignore_list, sgl_set_point_light) and calls sgl_render(viewport, camera, swap). A texture is made from an "intermediate map" of RGBA bytes, alpha 0 for opaque, by sgl_preprocess_texture and sgl_create_texture (sgl_map_16bit, sgl_map_trans16, sizes 32 to 256). The finished picture goes back to the game through an address callback (sgl_use_address_mode), which hands over the DirectDraw surface's memory. Find the game's scene builder from the first sgl_modify_list(SGL_DEFAULT_LIST, clear): what follows is the scene, in order.

gameframe-powervr keeps the same graph and draws it in software, with the PCX2's per-pixel rules (the nearest opaque surface per pixel, fog, translucent passes, light volumes) and the lighting of PowerVR's own driver (SGLMID4.dll: the texture effects, the spot light's fall-off): Sgl has the calls by their names. wrappers/sgl keeps the same graph and draws it with OpenGL for the game's own build.

Direct3D 3 execute buffers ​

Direct3D is not a card maker's API, but in 1997 a game's "Direct3D build" was written like the others: one renderer per interface. Direct3D 3 has no import of its own. The build imports only DirectDrawCreate (and often DirectDrawEnumerateA), and everything else goes through COM interfaces it gets with QueryInterface. That means IDirect3D from the IDirectDraw object, and the device itself (IDirect3DDevice) from the back-buffer surface, by the GUID that IDirect3D::FindDevice or EnumDevices chose. In the decompiler the calls show as indirect calls through a vtable: (*(*p + 0x20))(p, …). Name them by their offsets in the SDK's header order: IDirect3D +0x20 is FindDevice, IDirect3DDevice +0x18 CreateExecuteBuffer, +0x20 Execute, +0x38 EnumTextureFormats, +0x4c BeginScene, +0x50 EndScene. Read the dwSize the game writes first into each structure. It tells you which SDK the game was built with (D3DDEVICEDESC was 0xac bytes in DirectX 3 and grew in 5).

The drawing is in the execute buffer the game fills between Lock and Unlock. That buffer holds the vertices (D3DTLVERTEX, 32 bytes, when the game transforms and lights them itself) and a stream of D3DINSTRUCTION headers (bOpcode, bSize, wCount), each followed by its records: D3DOP_PROCESSVERTICES with COPY, D3DOP_STATERENDER pairs and D3DOP_TRIANGLE index triples. Find the code that writes opcode bytes (3 triangles, 8 render states, 9 process vertices, 11 exit) beside a moving write pointer: it is the game's whole renderer. The render states it writes are the table to document, by their D3DRENDERSTATETYPE numbers. Two things to check:

  • The order the stream is written in. A game can write its stream from the end of the buffer backwards, so that a batch's states, which are written when the next batch starts, land in front of it. The batches then run in the reverse of the order they were written in.
  • The capability bits behind each state. Games chose states by the device's D3DPRIMCAPS (alpha compare, blend factors, texture caps such as TRANSPARENCY), so every card gets its own variant of the stream. Pick one card and document the stream for it.

gameframe-d3d runs such buffers in software. Builder writes them, exec::parse reads them back (including a buffer dumped from the running game), and Device::execute draws them into an RGB565 target at any multiple of the game's resolution. It follows Direct3D's documented rasterisation rules: pixel centres at integer coordinates and the top-left fill convention. It also covers the texture blends, the alpha test, blending, the stipple pattern and vertex fog. Its rules come from Microsoft's public documentation, read as prose.

One game, one build per card ​

When a game ships a build per card, the builds share most of their code: the game logic, the file loaders, the sound. Compare them before you trace each:

  1. Match the builds with tools/triage/match_functions.py on their exports, and carry your labels from the reference build to the others.
  2. Find which data files each build reads. A file-name routine often picks an extension from a small table by a selector (.BL3, .BL4, …). Read the jump table and the value each build passes. Data in two variants (8-bit and 16-bit textures, say) often holds extra fields that only one build reads. A field that the reference build skips is not unused until every build has been checked.
  3. Trace the extra fields in the build that reads them. Find the reader's twin through match_functions.py, list its reads (call sites of the read routine with their sizes), and compare them with the reference build's list. The new read, its condition (a global set only for some files) and where its value is stored lead to its meaning. Strings next to the code that uses the value often name it ("Mirror_Faces").
  4. Write down what each build hands to the chip: which faces, which textures in which format, which states. Builds called "accelerated" sometimes accelerate very little.

Show what each card drew ​

In a port, each card is a device behind one interface, and the game's calls stay the same (The rendering pipeline, "The port as a virtual graphics card"). The player saw a different picture on each card: 8-bit software textures, 16-bit filtered textures on a Voodoo, affine textures on a ViRGE, halved texture pages where memory was short. A viewer or a port can offer the same choice as a setting: a "renderer profile" per card that uses the file variant, the texture formats and sizes, and the effects (reflections, fog) that card's build used. each chip crate's TEXTURE_FORMATS lists what that chip accepts; the game's own differences come from tracing its builds. gameframe-voodoo draws a 3dfx profile in software: hand it the triangles and Glide states the game's 3dfx build sends and it gives the picture at any size, with no Glide library. Its arithmetic follows 3dfx's register specifications (SST-1 / Voodoo Graphics revision 1.61, Voodoo2 revision 1.16; 3dfx allowed them to be copied whole and not for profit, so keep them beside the project, not in its repository): the combine units, the W buffer's floating format, fog and dither are drawn there as diagrams. Record which profile is the reference for exact comparisons (3D accelerators, "How to approach a game with several renderers").

Replacing the libraries ​

To run a card build without its card, or to draw it at a higher resolution, write a library with the same name and exports: Replacement libraries for a 1990s game.

Sources ​

  • 3dfx, Glide 2.x SDK and glide.h.
  • ATI Technologies, 3D RAGE Windows 95 Programmer's Guide, SDK-C02700 rev. 1.30, 1997.
  • VideoLogic, PowerVR driver package 3.10 (sgl.h, sglwin32.h, SGLMID4.dll), as shipped on POD Gold's disc (POWERVR/310C3.ZIP).
  • S3, S3D ToolKit SDK 2.6, S3DTK.H (published copy: https://github.com/kytulendu/S3DTK).
  • Microsoft, DirectX 3 and DirectX 5 SDK documentation, Direct3D Immediate Mode (execute buffers, D3DRENDERSTATETYPE, D3DTLVERTEX); and the Direct3D 9 pages "Rasterization Rules" and "Directly Mapping Texels to Pixels" for the fill convention and pixel centres.

Code: Apache 2.0. Content: CC0 1.0. No game material: bring your own copy.