How the patch works
This page explains what the patch changes in the game, and how its replacements are checked against the game’s own. Each section ends with technical details for readers who work on the game or on the patch.
The patch answers yes, then supplies the method
When the game starts, it asks the driver whether it offers ATI’s fragment shaders. The patch
answers that question itself, and answers yes unless you set decline = on. The
game then takes its ATI path on every driver.
The patch then supplies everything that path needs. The patch skips the step where the game builds its ATI fragment shaders. The game keeps calling the functions it always calls, and the patch handles those calls: when the game selects one of its ATI fragment shaders for a draw, the patch binds its own replacement instead. A small number of edits to the game’s code change where the game looks for features and which branches it takes, and remove game calls that the driver refuses. The rest of the game runs unchanged.
If the driver offers GL_NV_register_combiners2, the patch also hides it from the game, so that
the game doesn’t run its NVIDIA path alongside the ATI one.
Technical details
The game tests for GL_ATI_fragment_shader once, in GLRender::InitExtensions, and its other
choices follow from that answer, so that test is the one the patch replaces.
The patch clears GL_NV_register_combiners2 from the game’s capability word because the game’s
NVIDIA gates for soft shadows and screen effects test for it. On a driver that offers it, the
game runs its NVIDIA versions of those effects beside the ATI path, and clearing it keeps those
gates closed.
To supply the ATI path, the patch takes over each place the game reaches for it:
EnablePixelShaders, the dispatch pointer the game calls to set up the shading for each draw, so that the patch chooses which of its programs to bind.- The ATI entries in the game’s extension table, through which the game binds its shaders and
sets their constants. The patch turns those into binds of its programs and writes to
program.env. glEnableandglDisable, because the game turns shading on and off by enablingGL_FRAGMENT_SHADER_ATI, which means nothing to a driver without the extension.glFogi, because each of the patch’s programs has its fog equation built in, so the patch watches which equation the game selects to bind the matching program.glFinish, which the game calls before every texture upload on the ATI path, as a workaround for ATI hardware of 2003. On Intel’s Windows driver each call blocks for over four seconds, and the game rebuilds every texture when the renderer restarts. Nothing depends on the call, so the patch drops it.- The pixel buffer entries,
wglMakeCurrent, andglDrawBuffer, which the game uses to switch between its off-screen images and the window. The patch’s off-screen images are framebuffer objects, so it translates those calls. SwapBuffers,SetDeviceGammaRamp, and Bink’sBinkCopyToBuffer, for the brightness slider, as The patch applies the brightness slider to the picture explains.
The patch replaces the game’s shading programs
The game’s per-pixel lighting is a set of small programs: one for each kind of surface and one
for each screen effect. The patch replaces each with a program in GL_ARB_fragment_program,
which AMD’s, Intel’s, and NVIDIA’s drivers and Mesa all carry. Each surface program comes in
four versions, one for each way the game draws fog, because on this path a program applies fog
itself. Three surface programs blend the game’s fog in their own code instead, so their four
versions are the same.
The patch adds two programs of its own: one applies the brightness slider, and one shrinks images for the glow and soft shadows.
Technical details
All fourteen world shaders and nine effect shaders are translated to GL_ARB_fragment_program.
An ATI fragment shader runs before the fixed function fog stage, so the game’s shaders got fog without doing anything. An ARB fragment program replaces that stage, and its own fog option doesn’t work for this game: the game measures fog distance as the absolute depth in front of the camera, and Mesa doesn’t take that absolute value for programs that use the option, so fog vanishes. So each world shader applies fog itself, and is built against each of the four fog equations: off, linear, exp, and exp2. Applying it in the program also gives the same result on every driver.
The tests compare each replacement with the game’s program
Each replacement is translated from the game’s own program, as the game builds it. The tests draw the game’s program and the replacement side by side, and require the two images to match: exactly with fog off, and within one step of rounding with fog on. Where the patch corrects the game’s program, as Choose between the two looks describes, the tests require the images to differ, and to match exactly once the correction is undone. The tests read the game’s programs from your copy of the game each time they run, so none of the game’s programs are stored in this repository.
The game’s programs come in two kinds, ATI’s and NVIDIA’s, and so do the patch’s, as the two looks. The tests compare the Radeon look with the game’s ATI programs on Mesa, which still runs them. They compare most of the GeForce look with the game’s register combiner setups on an NVIDIA driver, which still runs those.
Technical details
The game has no shader text: it builds each ATI shader at startup from a sequence of
construction calls. arbfp-differential reads those calls out of the executable and replays
them on Mesa, which still implements GL_ATI_fragment_shader, so each comparison is against
the game’s own shader on a real driver. The tests pass on Mesa’s radeonsi driver. Twelve world
shaders come from those calls. Modes 0x08 and 0x09 have no ATI shader, so the patch
translates them from their register combiner setups.
With fog off, both programs run through the same pipeline, so the tests require identical pixels. With fog on, the game’s shader takes its fog from the driver’s fog stage, and the patch’s program computes it. The two round to a byte by different routes, so the tests allow one step. The tests that are meant to fail differ by tens to hundreds of steps. The three programs that blend their own fog are compared with fog off only, because the game draws them with the fixed function fog turned off.
The NVIDIA comparison reads each mode’s register combiner setup out of the executable and
replays it on an NVIDIA driver. It can’t cover modes 0x03 and 0x14, because the game’s
NVIDIA dispatcher has no case for them: those two blend their own fog, which NVIDIA cards did in
the last combiner stage instead. It can’t cover 0x18 either, because NVIDIA’s version of that
mode is built on texture shaders, which the comparison doesn’t replay. It holds modes 0x06
and 0x07 to their setups as repaired, because the game never finished them.
The patch enables lighting that only NVIDIA cards drew
Some of the game’s lighting ran only on NVIDIA cards, so even an ATI card of the time didn’t show it. The patch enables it on every card.
The game draws bump mapped creatures and objects with bump lighting only when it uses NVIDIA’s method. These include the Hutts, the Rancor, and some of the turret minigame’s models. On every other card, ATI’s included, it draws them with their texture and no bump lighting. The patch sends them down the bump lighting branch on every card.
The bump branches can’t fade, so while a bump mapped creature or head fades out, the patch sends it down the game’s plain branch, which fades with the rest of the body.
Technical details
Each NVIDIA-only branch is chosen by a call that asks which path the game is on. The patch answers those calls, and leaves every other use of the answer as the game wrote it.
In PartTriMesh::GetRenderPath, the answer to getATIFragmentShadersDisabled chooses between
path 5, RenderBumpMapped, which dispatches mode 0x06, and path 6, which draws the mesh with
its texture and no bump lighting. An edit makes the function return path 5.
In RenderSkinnedVP, the patch answers AurBumpyShinyAvailable and three calls to
getATIFragmentShadersDisabled from whether the skin is fading. The bump branches draw with
blend modes such as (GL_ONE, GL_ZERO), which ignore the skin’s alpha, so a fading skin stays
solid on those branches. The fade arrives through matLastAlpha, which PartTriMesh::Mat sets
just before each part is drawn.
k1_sys::arm lists every call that asks which path the game is on, and how the patch answers
each one.
The patch repairs lighting that no card drew
Some of the game’s lighting never worked on any card, NVIDIA’s included, because of bugs in the game. The shipped game uses almost none of it, but mods can:
- Bump lighting on lightmapped surfaces. Two lighting modes exist only as NVIDIA register combiner setups, with no ATI version. Their vertex programs are written in a format that only NVIDIA’s drivers read, and the game adds code to them that no driver accepts, NVIDIA’s included, so those surfaces draw nothing. The patch translates both modes, and builds their vertex programs from the developers’ own conversion of a related program. In the shipped game, only a test model that no level loads uses these modes.
- Normal mapped surfaces. The game has a route for normal maps on meshes without tangents, but the function that enables it always returns false. The patch answers that function instead, and repairs the vertex program that the skinned version of that route uses. It also corrects TGA normal maps, whose colour channels the game’s loader swaps. No mesh in the shipped game has a normal map.
- Reflective skins with a flat reflection map. Their vertex program exists only in NVIDIA’s format, so those skins vanished. The patch builds the program from the developers’ conversion of the cube map version. In the shipped game, only a test model uses this route.
Technical details
The lightmapped bump renderers build their vertex programs from !!VP1.0 text, NVIDIA’s format,
append the ARB light code that the game adds to every lit program, and load the result through
glProgramStringARB. NVIDIA’s driver accepts the !!VP1.0 text alone, but rejects it with the
light code appended, and Mesa rejects the text outright. So no card drew these surfaces.
The developers converted a related program to ARB themselves, at 0x0078E8C8. Its registers and
its hookup for the appended light code are what the rest of the game expects, so the patch builds
each replacement from it and changes only what differs. This repository holds none of the game’s
text, so the patch makes those changes at runtime, to the game’s own copy in memory. It swaps
each replacement in where the renderer stores the program it has just built, before the
renderer first enables it.
The same mechanism repairs three of RenderSkinnedVP’s vertex programs:
- The sphere map program exists only as
!!VP1.0text. It also reads its sphere coordinates from the wrong register and never normalises the reflection. The patch builds it from the developers’ conversion of the cube map version at0x007938C8, with the cube lookup replaced by the fixed function stage’s sphere map formula. - The normal map program subtracts each bone’s translation from the light and half vectors,
which are directions that a translation doesn’t move. It weights one term by the wrong bone,
and writes the normal map’s coordinate where mode
0x07doesn’t read it. The patch corrects each of these. - The bump light program doesn’t write the primary colour or the fog coordinate. Mesa reads an
unwritten primary colour as white, which mode
0x06adds to the light, so the skin’s texture was drawn whole and then added again.
k1_sys::vertex::builds lists every vertex program the game builds and enables, and a test
compiles each one that a draw can use, so a program that no driver accepts can’t go unnoticed.
The patch answers Material::HasNormalMap at all three of its calls, because they have to agree.
The call in MdlNodeTriMesh::InternalPostProcess decides at load whether to skip building
tangents for a mesh. The calls in PartTriMesh::GetRenderPath and RenderSkinnedVP send a mesh
without tangents to path 15, RenderNormalMapped, and to the skinned normal map branch, both of
which draw with mode 0x07. The answer is the game’s own test for a normal map: an isbumpmap 2
texture at 32 bits per texel.
The game’s TGA loader swaps the first and third byte of every texel to make RGB, and the normal map upload reads the data as BGRA, so the X and Z of every normal traded places. The patch swaps them back in the copy that the upload reads, which the mipmaps are built from as well.
Modes the patch leaves unshaded
NVIDIA’s dispatcher shades more modes than ATI’s. The patch shades the ones that a route can
reach once a shading method exists, and leaves the rest doing nothing, as ATI’s did. A mode that
does nothing has to stay that way, because binding something there takes the shading away from
the draw that follows it.
arbfp_dispatch::mode::inert_on_purpose gives the reason for each:
0x01,0x0F, and0x12areRenderBumpMapped’s passes for hardware without pixel shaders. That function runs only at a level of hardware that always has them.0x15isRenderNV1xEMLM, path 9, which the game chooses only below that level.0x0Aand0x0Bare specular exponents that the game never sets.0x22is skinned motion blur, which nothing in the game turns on.
Mode 0x17, skinning on the CPU, is the fallback for a skin drawn without a scene, which hasn’t
been seen to happen. It has no stated reason, so the patch logs a warning if it ever arrives. The
game never sends 0x04, 0x05, 0x0E, 0x10, 0x11, 0x19, 0x1B, or 0x1F, although
NVIDIA’s dispatcher has a case for each. k1_sys::dispatches lists every call that sends a mode.
The game’s ATI dispatcher does nothing for mode 0x1C, the teardown after 0x1D. ATI’s 0x1D
sets up cube maps on units 0 to 5, and the bump teardown, 0x0D, clears only units 1 to 3, so
cube maps stayed enabled on units 4 and 5 into later draws. NVIDIA’s fixed function stage stops
at four units, so only drivers with more, such as Mesa, ever had the problem. The patch’s 0x1C
clears units 4 and 5.
The screen effects draw into framebuffer objects
The game drew its glow, soft shadows, and screen effects into pixel buffers, an older kind of off-screen image. Modern drivers handle pixel buffers inconsistently, which is why those effects behaved differently from one machine to the next. The patch draws them into framebuffer objects, the modern equivalent.
The glow and the soft shadows each start by shrinking the screen into a small square. The game did this by reading a scattered sample of the screen’s pixels, which made the effects flicker as the camera moved. The patch shrinks the screen with a filter that takes every pixel into account, and stores the small images at a higher precision, so rounding doesn’t bring the flicker back.
Technical details
The off-screen targets use GL_RGBA16 where the driver can render to it, and eight bits per
channel, which is what the pixel buffers had, where it can’t.
RenderOverlayConvolution and FinishSoftShadowsATI draw the screen into a 512 by 512 square
with one bilinear read per texel, which past a ratio of two to one skips pixels. MipmapFilter
then halves the glow’s square to 64 by 64 with one read at exactly two to one, which averages
fixed pairs and steps the same way. Just before the game draws each square, the patch draws it
itself through a chain of DOWNSAMPLE steps that weight each source pixel by a triangle one
output pixel wide on either side, so every screen pixel counts. It then turns colour writes off,
so that the game’s own quad leaves the result alone. The halving steps bind the downsample in
place of the decal shader.
The glow’s bright pass is a threshold, so it runs on each screen pixel before any averaging. That way a small light stays above the threshold at high resolutions, and keeps its glow.
The patch applies the brightness slider to the picture
The game applied brightness by asking the display to use a brightness curve, which a driver can ignore. The patch keeps the curve the game asked for and applies it to the finished picture, and to movies as they are decoded.
Technical details
CAurInternal::ResetGammaRamp turns the slider position into a pow(i / 255, gamma) table and
passes it to SetDeviceGammaRamp. The patch takes over that import: it keeps the table, and
passes a flat table to the display so that the display applies nothing. The patch then applies
the table to each frame as a dependent texture read in a pass at SwapBuffers. It applies the
table to movies in BinkCopyToBuffer, because PlayMovieLoop copies each movie frame straight
to the window, and UpdateScreen skips the swap while a movie plays.
The patch runs the same path on every card
The patch doesn’t check which card you have. It replaces the game’s shading even where the game’s own path works, on NVIDIA and on Mesa, so every player runs the same code, and that code is the code everyone tests.